package com.tyrone.captcha.service;

import com.tyrone.captcha.util.IdWorker;

import com.tyrone.captcha.entity.vo.ImageCodeVo;
import com.tyrone.captcha.entity.constant.SlideImageCodeConstant;
import org.apache.axis.encoding.Base64;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.*;

import java.util.ArrayList;
import java.util.concurrent.TimeUnit;


/**
 * @author xieyl
 * @create 2021/10/19 21:55
 */
@Service
public class SlideImageCodeService {

    @Autowired
    private IdWorker idWorker;

    @Autowired
    private RedisTemplate redisTemplate;

    private static final String IMAGE_CODE_KEY_PREFIX = "IMAGE_CODE_";

    /**
     * 图片偏移容错量
     */
    private static final int FAULT_TOLERANCE = 3;


    /**
     * 原始图片比例
     */
    private static final int SOURCE_PICTURE_WIDTH = 680;
    private static final int SOURCE_PICTURE_HEIGHT = 390;

    /**
     * 小图的宽---模板图的宽度
     */
    private static final int TAILORING_WIDTH = 136;
    /**
     * 小图的高---模板图的高度
     */
    private static final int TAILORING_HEIGHT = 136;

    /**
     * 拼图比列
     */
    private static final int JIGSAW_WIDTH = 88;
    private static final int JIGSAW_HEIGHT = 88;

    private static final int JIGSAW_MARGIN = (TAILORING_HEIGHT - JIGSAW_WIDTH) / 2;

    /**
     * 阴影宽度
     */
    private static final int SHADOW_WIDTH = 3;
    /**
     * 图片边缘亮色（黄色）宽度。
     */
    private static final int LIGHT_HEIGHT_WIDTH = 2;
    /**
     * 圆弧直径
     */
    private static final int CIRCLE_DIAMETER = 30;
    /**
     * 正方形的四边：{ 左边：0,上边：1,右边：2,下边：3 }
     */
    private static final int LEFT_EDGE = 0;
    private static final int TOP_EDGE = 1;
    private static final int RIGHT_EDGE = 2;
    private static final int BOTTOM_EDGE = 3;
    /**
     * 凸圆：0 凹圆：1
     */
    private static final int CONVEX_CIRCLE = 0;
    private static final int CONCAVE_CIRCLE = 1;
    private static final Color SHADOW_COLOR = new Color(31, 29, 29, 20);
    private static final Color OUTER_SHADOW_COLOR = new Color(0, 0, 0, 255);
    private static final Color INNER_SHADOW_COLOR = new Color(0, 0, 0, 204);
    private static final Color WATER_MARK_PADDING_COLOR = new Color(255, 255, 255, 255);

    //----------------临时全局变量区域----------------------


    /**
     * 随机X位置---位于原图的X位置
     */
    private ThreadLocal<Integer> locationX = new ThreadLocal<>();

    /**
     * 随机Y位置----位于原图的Y位置
     */
    private ThreadLocal<Integer> locationY = new ThreadLocal<>();


    private ThreadLocal<BufferedImage> shadowLayer = new ThreadLocal<>();


    /**
     * 创建图片验证码
     *
     * @return
     * @throws IOException
     */
    public ImageCodeVo createImageCode() throws IOException {
        /**
         * 获取验证码原图
         */
        BufferedImage picture = getPicture();

        /**
         * 根据验证码原图获取模板图
         */
        BufferedImage templatePic = getTemplatePic(picture);
        /**
         * 获取抠图形状
         */
        Shape picShape = getPicShape();
        /**
         * 根据抠图形状，从模板图中扣出拼图
         */
        BufferedImage jigsaw = handlerPic(templatePic, picShape);
        /**
         * 大图添加阴影水印
         */
        BufferedImage bottomImage = addWaterMark(shadowLayer.get(), picture, picShape);
        ByteArrayOutputStream bottomImageStream = new ByteArrayOutputStream();
        ImageIO.write(bottomImage, "png", bottomImageStream);
        String bottomImageBase64 = Base64.encode(bottomImageStream.toByteArray());
        bottomImageStream.flush();
        bottomImageStream.close();
        ByteArrayOutputStream jigsawStream = new ByteArrayOutputStream();
        ImageIO.write(jigsaw, "png", jigsawStream);
        String jigsawBase64 = Base64.encode(jigsawStream.toByteArray());
        jigsawStream.flush();
        jigsawStream.close();
        Long id = idWorker.nextId();
        /**
         * 设置过期时间三分钟
         */
        redisTemplate.opsForValue().set(IMAGE_CODE_KEY_PREFIX + id, locationX.get(), 60, TimeUnit.SECONDS);
        ImageCodeVo imageCodeVo = ImageCodeVo.builder()
                .bottomImageBase64(bottomImageBase64)
                .jigsawBase64(jigsawBase64)
                .id(id.toString())
                .locationY(locationY.get())
                .build();
        locationX.remove();
        locationY.remove();
        shadowLayer.remove();
        return imageCodeVo;
    }

    /**
     * 校验图片验证码
     *
     * @param id
     * @param offset
     * @return
     */
    public boolean checkImageCode(String id, Integer offset) {
        Integer integer = (Integer) redisTemplate.opsForValue().get(IMAGE_CODE_KEY_PREFIX + id);
        if (integer == null) {
            return false;
        } else {
            if (Math.abs(integer - offset) <= FAULT_TOLERANCE) {
                return true;
            } else {
                return false;
            }
        }
    }


    /**
     * 获取图片BufferImage
     * <p>
     * 1.读取一张照片
     */
    public BufferedImage getPicture() throws IOException {
        String imageName = "image" + RandomUtils.nextInt(5) + ".png";
        String imageString = (String) redisTemplate.opsForValue().get(IMAGE_CODE_KEY_PREFIX + imageName);
        if(StringUtils.isEmpty(imageString)){
            FileInputStream fileInputStream = new FileInputStream("src/main/resources/img/" + imageName);
            InputStream is = new BufferedInputStream(fileInputStream);
            BufferedImage read = ImageIO.read(is);
            String imageStringToRedis = encodeToString(read);
            redisTemplate.opsForValue().set(imageName,imageStringToRedis);
            return read;
        }
        return decodeToImage(imageString);
    }

    /**
     * 获取一张要修剪的正方形矩形模板图 (136*136)
     * 尽量选在右半部分
     */
    public BufferedImage getTemplatePic(BufferedImage image) {
        /**
         *  X ∈ [Width/2-Padding，Width-Template-Padding]   Y ∈ [Padding,Height-Template-Padding]
         */
        locationX.set(RandomUtils.nextInt(524 - 350 + 1) + 350);
        locationY.set(RandomUtils.nextInt(234 - 20 + 1) + 20);
        BufferedImage subimage = image.getSubimage(locationX.get(), locationY.get(), TAILORING_WIDTH, TAILORING_HEIGHT);
        return subimage;
    }

    /**
     * 获取拼图形状
     * 正方形的四边：{ 左边：0,上边：1,右边：2,下边：3 }
     * 凸圆：0 凹圆：1
     *
     * @return
     */
    public Shape getPicShape() {
        int edge = RandomUtils.nextInt(4);
        int circularTypeOne = RandomUtils.nextInt(2);
        int edgeTwo = RandomUtils.nextInt(4);
        while (edgeTwo == edge) {
            edgeTwo = RandomUtils.nextInt(4);
        }
        int circularTypeTwo = RandomUtils.nextInt(2);
        Area area = new Area();
        Shape square = new Rectangle2D.Double(JIGSAW_MARGIN, JIGSAW_MARGIN, JIGSAW_WIDTH, JIGSAW_HEIGHT);
        area.add(new Area(square));
        createCircular(area, edge, circularTypeOne);
        createCircular(area, edgeTwo, circularTypeTwo);
        return area;
    }

    /**
     * 中心矩形面积添加或减去半圆
     *
     * @param area
     * @param edge
     * @param circularType
     * @return
     */
    public Shape createCircular(Area area, int edge, int circularType) {
        Shape circle = null;
        /**
         * cursor（游标）：圆的右上角坐标随机化  cursor ∈[Padding+Margin,Padding+Width-Diameter-Margin]
         */
        int cursor = RandomUtils.nextInt(74 - 32 + 1) + 32;
        int margin = JIGSAW_MARGIN - (CIRCLE_DIAMETER / 2);
        if (edge == LEFT_EDGE) {
            circle = new Arc2D.Double(margin, cursor, CIRCLE_DIAMETER, CIRCLE_DIAMETER, 0, 360, Arc2D.OPEN);
        } else if (edge == TOP_EDGE) {
            circle = new Arc2D.Double(cursor, margin, CIRCLE_DIAMETER, CIRCLE_DIAMETER, 0, 360, Arc2D.OPEN);
        } else if (edge == RIGHT_EDGE) {
            circle = new Arc2D.Double(JIGSAW_WIDTH + margin, cursor, CIRCLE_DIAMETER, CIRCLE_DIAMETER, 0, 360, Arc2D.OPEN);
        } else {
            circle = new Arc2D.Double(cursor, JIGSAW_WIDTH + margin, CIRCLE_DIAMETER, CIRCLE_DIAMETER, 0, 360, Arc2D.OPEN);
        }
        /**
         * 判断圆的类型，区域做加减
         */
        if (circularType == CONVEX_CIRCLE) {
            area.add(new Area(circle));
        } else {
            area.subtract(new Area(circle));
        }
        return area;
    }

    /**
     * 处理模板图生成不规则图片
     */
    public BufferedImage handlerPic(BufferedImage template, Shape imageShape) {

        BufferedImage cut = template;
        Shape shape = imageShape;
        Graphics2D graphics = cut.createGraphics();
        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        graphics.draw(shape);
        BufferedImage bfm = new BufferedImage(TAILORING_WIDTH, TAILORING_HEIGHT, BufferedImage.TYPE_INT_ARGB);
        shadowLayer.set(new BufferedImage(TAILORING_WIDTH, TAILORING_HEIGHT, BufferedImage.TYPE_INT_ARGB));
        for (int i = 0; i < TAILORING_WIDTH; i++) {
            for (int j = 0; j < TAILORING_HEIGHT; j++) {
                if (shape.contains(i, j)) {
                    bfm.setRGB(i, j, template.getRGB(i, j));
                }
                if (shape.contains(i, j)) {
                    shadowLayer.get().setRGB(i, j, Color.black.getRGB());
                }
            }
        }
        BufferedImage resultImgBuff = dealLightAndShadow(bfm, shape);
        return resultImgBuff;

    }

    /**
     * 在大图增加阴影部分，即填充部分
     *
     * @param image
     * @param sourceImage
     * @return
     */
    public BufferedImage addWaterMark(BufferedImage image, BufferedImage sourceImage, Shape shape) {
        BufferedImage source = sourceImage;
        Graphics2D graphics = source.createGraphics();
        graphics.setColor(Color.white);
        graphics.setStroke(new BasicStroke(SHADOW_WIDTH));
        graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.6F));
        BufferedImage waterMark = image;
        handler(waterMark.createGraphics(), SHADOW_WIDTH, shape);
        shadowHandle(waterMark.createGraphics(), SHADOW_WIDTH, shape);
        shadowHandleBlack(waterMark.createGraphics(), SHADOW_WIDTH, shape);
        graphics.drawImage(waterMark, locationX.get(), locationY.get(), null);
        graphics.dispose();
        return source;
    }

    /**
     * 处理拼图的边框效果与阴影
     *
     * @param bfm
     * @param shape
     * @return
     */
    private BufferedImage dealLightAndShadow(BufferedImage bfm, Shape shape) {
        //创建新的透明图层，该图层用于边缘化阴影， 将生成的小图合并到该图上
        BufferedImage buffimg = ((Graphics2D) bfm.getGraphics()).getDeviceConfiguration().createCompatibleImage(TAILORING_WIDTH, TAILORING_HEIGHT, Transparency.TRANSLUCENT);
        Graphics2D graphics2D = buffimg.createGraphics();
        Graphics2D g2 = (Graphics2D) bfm.getGraphics();
        //拼图边框处理
        paintBorderGlow(g2, LIGHT_HEIGHT_WIDTH, shape);
        //新图层添加阴影
        paintBorderShadow(graphics2D, SHADOW_WIDTH, shape);
        graphics2D.drawImage(bfm, 0, 0, null);
        //用白色描边
        graphics2D.setColor(new Color(255, 255, 255));
        graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        graphics2D.setStroke(new BasicStroke(2));
        graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP));
        graphics2D.draw(shape);
        return buffimg;
    }

    /**
     * 拼图边框边框渲染
     * 实现逻辑，通过循环，降低透明度，并且添加渐变 ，实现毛玻璃效果
     *
     * @param g2
     * @param glowWidth
     * @param clipShape
     */
    private void paintBorderGlow(Graphics2D g2, int glowWidth, Shape clipShape) {
        int gw = glowWidth * 16;
        g2.setColor(Color.WHITE);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.draw(clipShape);
        for (int i = gw; i >= 2; i -= 2) {
            float pct = (float) (gw - i) / (gw - 1);
            Color borderRander = new Color(224, 213, 213, 89 - i * 2);
            g2.setPaint(new GradientPaint(0, 0, borderRander, TAILORING_WIDTH, TAILORING_HEIGHT, borderRander));
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, pct));
            g2.setStroke(new BasicStroke(i));
            g2.draw(clipShape);
        }
    }

    /**
     * 拼图阴影处理
     *
     * @param g2
     * @param shadowWidth
     * @param clipShape
     */
    private void paintBorderShadow(Graphics2D g2, int shadowWidth, Shape clipShape) {
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        int sw = shadowWidth * 8;
        for (int i = sw; i >= 2; i -= 2) {
            float pct = (float) (sw - i) / (sw - 1);
            //pct<03. 用于去掉阴影边缘白边，  pct>0.8用于去掉过深的色彩， 如果使用Color.lightGray. 可去掉pct>0.8
            if (pct < 0.2) {
                continue;
            }
            g2.setColor(new Color(12, 12, 12, 20 - i));
            g2.setStroke(new BasicStroke(i + 1));
            g2.draw(clipShape);
        }
    }

    /**
     * 处理水印白边情况
     *
     * @param g2
     * @param shadowWidth
     * @param clipShape
     */
    private void shadowHandle(Graphics2D g2, int shadowWidth, Shape clipShape) {
        g2.setColor(Color.black);
        g2.setStroke(new BasicStroke(1));
        g2.draw(clipShape);
    }

    /**
     * 水印边框描白
     *
     * @param g2
     * @param shadowWidth
     * @param clipShape
     */
    private void shadowHandleBlack(Graphics2D g2, int shadowWidth, Shape clipShape) {
        g2.setColor(WATER_MARK_PADDING_COLOR);
        g2.setStroke(new BasicStroke(1));
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1F));
        g2.draw(clipShape);
    }

    /**
     * 水印内发光
     *
     * @param g2
     * @param shadowWidth
     * @param clipShape
     */
    public void handler(Graphics2D g2, int shadowWidth, Shape clipShape) {
        int gw = shadowWidth * 8;
        for (int i = gw; i >= 2; i -= 2) {
            float pct = (float) (gw - i) / (gw - 1);
            Color mixHi = getMixedColor(OUTER_SHADOW_COLOR, pct, INNER_SHADOW_COLOR, 1.0f - pct);
            Color mixLo = getMixedColor(OUTER_SHADOW_COLOR, pct, INNER_SHADOW_COLOR, 1.0f - pct);
            g2.setPaint(new GradientPaint(0.0f, 35 * 0.25f, mixHi, 0, 35, mixLo));
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, pct));
            g2.setStroke(new BasicStroke(i));
            g2.draw(clipShape);
        }
    }

    /**
     * 色彩处理
     *
     * @param c1
     * @param pct1
     * @param c2
     * @param pct2
     * @return
     */
    private static Color getMixedColor(Color c1, float pct1, Color c2, float pct2) {
        float[] clr1 = c1.getComponents(null);
        float[] clr2 = c2.getComponents(null);
        for (int i = 0; i < clr1.length; i++) {
            clr1[i] = (clr1[i] * pct1) + (clr2[i] * pct2);
        }
        return new Color(clr1[0], clr1[1], clr1[2], clr1[3]);
    }


    /**
     * 轨迹校验
     *
     * @param trajectory
     * @return
     */
    // TODO: 2021/11/18 先简单用坐标的个数进行人机校验，后续在考虑其他方法
    public boolean trajectoryVerification(ArrayList<Point> trajectory) {
        if (trajectory.size() > SlideImageCodeConstant.TRAJECTORY_THRESHOLD_LOW && trajectory.size() < SlideImageCodeConstant.TRAJECTORY_THRESHOLD_HIGH) {
            return true;
        }
        return false;
    }

    public  BufferedImage decodeToImage(String imageString) {//解码
        BufferedImage image = null;
        try {
            byte[] decode = Base64.decode(imageString);
            ByteArrayInputStream in = new ByteArrayInputStream(decode);    //将b作为输入流；
            image = ImageIO.read(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return image;
    }

    public  String encodeToString(BufferedImage image) {
        String imageString = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            ImageIO.write(image, "png", bos);
            imageString = Base64.encode(bos.toByteArray());
            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return imageString;
    }
}
